<!DOCTYPE html>
<title>Node.moveBefore</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<div id="log"></div>
<!-- First test shared pre-insertion checks that work similarly for replaceChild
     and moveBefore -->
<script>
  var insertFunc = Node.prototype.moveBefore;
</script>
<script src="../../pre-insertion-validation-hierarchy.js"></script>
<script>
preInsertionValidateHierarchy("moveBefore");

test(function() {
  // WebIDL: first argument.
  assert_throws_js(TypeError, function() { document.body.moveBefore(null, null) })
  assert_throws_js(TypeError, function() { document.body.moveBefore(null, document.body.firstChild) })
  assert_throws_js(TypeError, function() { document.body.moveBefore({'a':'b'}, document.body.firstChild) })
}, "Calling moveBefore with a non-Node first argument must throw TypeError.")

test(function() {
  // WebIDL: second argument.
  assert_throws_js(TypeError, function() { document.body.moveBefore(document.createTextNode("child")) })
  assert_throws_js(TypeError, function() { document.body.moveBefore(document.createTextNode("child"), {'a':'b'}) })
}, "Calling moveBefore with second argument missing, or other than Node, null, or undefined, must throw TypeError.")

test(() => {
  assert_false("moveBefore" in document.doctype, "moveBefore() not on DocumentType");
  assert_false("moveBefore" in document.createTextNode("text"), "moveBefore() not on TextNode");
  assert_false("moveBefore" in new Comment("comment"), "moveBefore() not on CommentNode");
  assert_false("moveBefore" in document.createProcessingInstruction("foo", "bar"), "moveBefore() not on ProcessingInstruction");
}, "moveBefore() method does not exist on non-ParentNode Nodes");

// Pre-move validity, step 1:
// "If either parent or node are not connected, then throw a
// "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
  const connectedTarget = document.body.appendChild(document.createElement('div'));
  const disconnectedDestination = document.createElement('div');
  t.add_cleanup(() => connectedTarget.remove());

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    disconnectedDestination.moveBefore(connectedTarget, null);
  });
}, "moveBefore() on disconnected parent throws a HierarchyRequestError");
test(t => {
  const connectedDestination = document.body.appendChild(document.createElement('div'));
  const disconnectedTarget = document.createElement('div');
  t.add_cleanup(() => connectedDestination.remove());

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    connectedDestination.moveBefore(disconnectedTarget, null);
  });
}, "moveBefore() with disconnected target node throws a HierarchyRequestError");

// Pre-move validity, step 2:
// "If parent’s shadow-including root is not the same as node’s shadow-including
// "root, then throw a "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
  const iframe = document.createElement('iframe');
  document.body.append(iframe);
  const connectedCrossDocChild = iframe.contentDocument.createElement('div');
  const connectedLocalParent = document.querySelector('div');
  t.add_cleanup(() => iframe.remove());

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    connectedLocalParent.moveBefore(connectedCrossDocChild, null);
  });
}, "moveBefore() on a cross-document target node throws a HierarchyRequestError");

// Pre-move validity, step 3:
// "If parent is not a Document, DocumentFragment, or Element node, then throw a
// "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
  const iframe = document.body.appendChild(document.createElement('iframe'));
  const innerBody = iframe.contentDocument.querySelector('body');

  assert_throws_dom("HIERARCHY_REQUEST_ERR", iframe.contentWindow.DOMException, () => {
    // Moving the body into the same place that it already is, which is a valid
    // action in the normal case, when moving an Element directly under the
    // document. This is not `moveBefore()`-specific behavior; it is consistent
    // with traditional Document insertion rules, just like `insertBefore()`.
    iframe.contentDocument.moveBefore(innerBody, null);
  });
}, "moveBefore() into a Document throws a HierarchyRequestError");
test(t => {
  const iframe = document.body.appendChild(document.createElement('iframe'));
  const comment = iframe.contentDocument.createComment("comment");
  iframe.contentDocument.body.append(comment);

  iframe.contentDocument.moveBefore(comment, null);
  assert_equals(comment.parentNode, iframe.contentDocument);
}, "moveBefore() CharacterData into a Document");

// Pre-move validity, step 4:
// "If node is a host-including inclusive ancestor of parent, then throw a
// "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
  const parentDiv = document.body.appendChild(document.createElement('div'));
  const childDiv = parentDiv.appendChild(document.createElement('div'));
  t.add_cleanup(() => {
    parentDiv.remove();
    childDiv.remove();
  });

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    parentDiv.moveBefore(parentDiv, null);
  }, "parent moving itself");

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    childDiv.moveBefore(parentDiv, null);
  }, "Moving parent into immediate child");

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    childDiv.moveBefore(document.body, null);
  }, "Moving grandparent into grandchild");

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    document.body.moveBefore(document.documentElement, childDiv);
  }, "Moving documentElement (<html>) into a deeper child");
}, "moveBefore() with node being an inclusive ancestor of parent throws a " +
   "HierarchyRequestError");

// Pre-move validity, step 5:
// "If node is not an Element or a CharacterData node, then throw a
// "HierarchyRequestError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
  assert_true(document.doctype.isConnected);
  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    document.body.moveBefore(document.doctype, null);
  }, "DocumentType throws");

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    document.body.moveBefore(new DocumentFragment(), null);
  }, "DocumentFragment throws");

  const doc = document.implementation.createHTMLDocument("title");
  assert_true(doc.isConnected);
  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    document.body.moveBefore(doc, null);
  });
}, "moveBefore() with a non-{Element, CharacterData} throws a HierarchyRequestError");
promise_test(async t => {
  const text = new Text("child text");
  document.body.prepend(text);

  const childElement = document.createElement('p');
  document.body.prepend(childElement);

  const comment = new Comment("comment");
  document.body.prepend(comment);

  t.add_cleanup(() => {
    text.remove();
    childElement.remove();
    comment.remove();
  });

  // Wait until style is computed once, then continue after. This is necessary
  // to reproduce a Chromium crash regression with moving Comment nodes in the
  // DOM.
  await new Promise(r => {
    requestAnimationFrame(() => requestAnimationFrame(() => r()));
  });

  document.body.moveBefore(text, null);
  assert_equals(document.body.lastChild, text);

  document.body.moveBefore(childElement, null);
  assert_equals(document.body.lastChild, childElement);

  document.body.moveBefore(text, null);
  assert_equals(document.body.lastChild, text);

  document.body.moveBefore(comment, null);
  assert_equals(document.body.lastChild, comment);
}, "moveBefore with an Element or CharacterData succeeds");
test(t => {
  const p = document.createElement('p');
  p.textContent = "Some content";
  document.body.prepend(p);

  const text_node = p.firstChild;

  // The Text node is *inside* the paragraph.
  assert_equals(text_node.textContent, "Some content");
  assert_not_equals(document.body.lastChild, text_node);

  t.add_cleanup(() => {
    text_node.remove();
    p.remove();
  });

  document.body.moveBefore(p.firstChild, null);
  assert_equals(document.body.lastChild, text_node);
}, "moveBefore on a paragraph's Text node child");

// Pre-move validity, step 6:
// "If child is non-null and its parent is not parent, then throw a
// "NotFoundError" DOMException."
//
// https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity
test(t => {
  const a = document.body.appendChild(document.createElement("div"));
  const b = document.body.appendChild(document.createElement("div"));
  const c = document.body.appendChild(document.createElement("div"));

  t.add_cleanup(() => {
    a.remove();
    b.remove();
    c.remove();
  });

  assert_throws_dom("NotFoundError", () => {
    a.moveBefore(b, c);
  });
}, "moveBefore with reference child whose parent is NOT the destination " +
   "parent (context node) throws a NotFoundError.")

test(() => {
  const a = document.body.appendChild(document.createElement("div"));
  const b = document.createElement("div");
  const c = document.createElement("div");
  a.append(b);
  a.append(c);
  assert_array_equals(a.childNodes, [b, c]);
  assert_equals(a.moveBefore(c, b), undefined, "moveBefore() returns undefined");
  assert_array_equals(a.childNodes, [c, b]);
}, "moveBefore() returns undefined");

test(() => {
  const a = document.body.appendChild(document.createElement("div"));
  const b = document.createElement("div");
  const c = document.createElement("div");
  a.append(b);
  a.append(c);
  assert_array_equals(a.childNodes, [b, c]);
  a.moveBefore(b, b);
  assert_array_equals(a.childNodes, [b, c]);
  a.moveBefore(c, c);
  assert_array_equals(a.childNodes, [b, c]);
}, "Moving a node before itself should not move the node");

test(() => {
  const disconnectedOrigin = document.createElement('div');
  const disconnectedDestination = document.createElement('div');
  const p = disconnectedOrigin.appendChild(document.createElement('p'));

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => {
    disconnectedDestination.moveBefore(p, null);
  });
}, "Moving a node from a disconnected container to a disconnected new parent " +
   "without a shared ancestor throws a HIERARCHY_REQUEST_ERR");

test(() => {
  const disconnectedOrigin = document.createElement('div');
  const disconnectedDestination = disconnectedOrigin.appendChild(document.createElement('div'));
  const p = disconnectedOrigin.appendChild(document.createElement('p'));

  disconnectedDestination.moveBefore(p, null);

  assert_equals(disconnectedDestination.firstChild, p, "<p> Was successfully moved");
}, "Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds");

test(() => {
  const disconnectedOrigin = document.createElement('div');
  const disconnectedHost = disconnectedOrigin.appendChild(document.createElement('div'));
  const p = disconnectedOrigin.appendChild(document.createElement('p'));
  const shadow = disconnectedHost.attachShadow({mode: "closed"});
  const disconnectedDestination = shadow.appendChild(document.createElement('div'));

  disconnectedDestination.moveBefore(p, null);

  assert_equals(disconnectedDestination.firstChild, p, "<p> Was successfully moved");
}, "Moving a node from a disconnected container to a disconnected new parent in the same tree succeeds," +
   "also across shadow-roots");

test(() => {
  const disconnectedOrigin = document.createElement('div');
  const connectedDestination = document.body.appendChild(document.createElement('div'));
  const p = disconnectedOrigin.appendChild(document.createElement('p'));

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => connectedDestination.moveBefore(p, null));
}, "Moving a node from disconnected->connected throws a HIERARCHY_REQUEST_ERR");

test(() => {
  const connectedOrigin = document.body.appendChild(document.createElement('div'));
  const disconnectedDestination = document.createElement('div');
  const p = connectedOrigin.appendChild(document.createElement('p'));

  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => disconnectedDestination.moveBefore(p, null));
}, "Moving a node from connected->disconnected throws a HIERARCHY_REQUEST_ERR");

promise_test(async t => {
  let reactions = [];
  const element_name = `ce-${performance.now()}`;
  customElements.define(element_name,
    class MockCustomElement extends HTMLElement {
      connectedMoveCallback() { reactions.push("connectedMove"); }
      connectedCallback() { reactions.push("connected"); }
      disconnectedCallback() { reactions.push("disconnected"); }
    });

  const oldParent = document.createElement('div');
  const newParent = oldParent.appendChild(document.createElement('div'));
  const element = oldParent.appendChild(document.createElement(element_name));
  t.add_cleanup(() => {
    element.remove();
    newParent.remove();
    oldParent.remove();
  });

  // Wait a microtask to let any custom element reactions run (should be none,
  // since the initial parent is disconnected).
  await Promise.resolve();

  newParent.moveBefore(element, null);
  await Promise.resolve();
  assert_array_equals(reactions, []);
}, "No custom element callbacks are run during disconnected moveBefore()");

// This is a regression test for a Chromium crash: https://crbug.com/388934346.
test(t => {
  // This test caused a crash in Chromium because after the detection of invalid
  // /node hierarchy, and throwing the JS error, we did not return from native
  // code, and continued to operate on the node tree on bad assumptions.
  const outer = document.createElement('div');
  const div = outer.appendChild(document.createElement('div'));
  assert_throws_dom("HIERARCHY_REQUEST_ERR", () => div.moveBefore(outer, null));
}, "Invalid node hierarchy with null old parent does not crash");

test(t => {
  const outerDiv = document.createElement('div');
  const innerDiv = outerDiv.appendChild(document.createElement('div'));
  const iframe = innerDiv.appendChild(document.createElement('iframe'));
  outerDiv.moveBefore(iframe, null);
}, "Move disconnected iframe does not crash");
</script>
